/*
 * Copyright 2012-present the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.tomcat.autoconfigure;

import java.io.File;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;

import org.jspecify.annotations.Nullable;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.util.unit.DataSize;

/**
 * Tomcat server properties.
 *
 * @author Dave Syer
 * @author Stephane Nicoll
 * @author Andy Wilkinson
 * @author Ivan Sopov
 * @author Marcos Barbero
 * @author Eddú Meléndez
 * @author Quinten De Swaef
 * @author Venil Noronha
 * @author Aurélien Leboulanger
 * @author Brian Clozel
 * @author Olivier Lamy
 * @author Chentao Qu
 * @author Artsiom Yudovin
 * @author Andrew McGhie
 * @author Rafiullah Hamedy
 * @author Dirk Deyne
 * @author HaiTao Zhang
 * @author Victor Mandujano
 * @author Chris Bono
 * @author Parviz Rozikov
 * @author Florian Storz
 * @author Michael Weidmann
 * @author Lasse Wulff
 * @since 4.0.0
 */
@ConfigurationProperties("server.tomcat")
public class TomcatServerProperties {

	/**
	 * Tomcat base directory. If not specified, a temporary directory is used.
	 */
	private @Nullable File basedir;

	/**
	 * Delay between the invocation of backgroundProcess methods. If a duration suffix is
	 * not specified, seconds will be used.
	 */
	@DurationUnit(ChronoUnit.SECONDS)
	private Duration backgroundProcessorDelay = Duration.ofSeconds(10);

	/**
	 * Maximum size of the form content in any HTTP post request.
	 */
	private DataSize maxHttpFormPostSize = DataSize.ofMegabytes(2);

	/**
	 * Maximum per-part header size permitted in a multipart/form-data request. Requests
	 * that exceed this limit will be rejected. A value of less than 0 means no limit.
	 */
	private DataSize maxPartHeaderSize = DataSize.ofBytes(512);

	/**
	 * Maximum total number of parts permitted in a multipart/form-data request. Requests
	 * that exceed this limit will be rejected. A value of less than 0 means no limit.
	 */
	private int maxPartCount = 10;

	/**
	 * Maximum amount of request body to swallow.
	 */
	private DataSize maxSwallowSize = DataSize.ofMegabytes(2);

	/**
	 * Whether requests to the context root should be redirected by appending a / to the
	 * path. When using SSL terminated at a proxy, this property should be set to false.
	 */
	private Boolean redirectContextRoot = true;

	/**
	 * Whether HTTP 1.1 and later location headers generated by a call to sendRedirect
	 * will use relative or absolute redirects.
	 */
	private boolean useRelativeRedirects;

	/**
	 * Character encoding to use to decode the URI.
	 */
	private Charset uriEncoding = StandardCharsets.UTF_8;

	/**
	 * Maximum number of connections that the server accepts and processes at any given
	 * time. Once the limit has been reached, the operating system may still accept
	 * connections based on the "acceptCount" property.
	 */
	private int maxConnections = 8192;

	/**
	 * Maximum queue length for incoming connection requests when all possible request
	 * processing threads are in use.
	 */
	private int acceptCount = 100;

	/**
	 * Maximum number of idle processors that will be retained in the cache and reused
	 * with a subsequent request. When set to -1 the cache will be unlimited with a
	 * theoretical maximum size equal to the maximum number of connections.
	 */
	private int processorCache = 200;

	/**
	 * Time to wait for another HTTP request before the connection is closed. When not set
	 * the connectionTimeout is used. When set to -1 there will be no timeout.
	 */
	private @Nullable Duration keepAliveTimeout;

	/**
	 * Maximum number of HTTP requests that can be pipelined before the connection is
	 * closed. When set to 0 or 1, keep-alive and pipelining are disabled. When set to -1,
	 * an unlimited number of pipelined or keep-alive requests are allowed.
	 */
	private int maxKeepAliveRequests = 100;

	/**
	 * List of additional patterns that match jars to ignore for TLD scanning. The special
	 * '?' and '*' characters can be used in the pattern to match one and only one
	 * character and zero or more characters respectively.
	 */
	private List<String> additionalTldSkipPatterns = new ArrayList<>();

	/**
	 * List of additional unencoded characters that should be allowed in URI paths. Only
	 * "< > [ \ ] ^ ` { | }" are allowed.
	 */
	private List<Character> relaxedPathChars = new ArrayList<>();

	/**
	 * List of additional unencoded characters that should be allowed in URI query
	 * strings. Only "< > [ \ ] ^ ` { | }" are allowed.
	 */
	private List<Character> relaxedQueryChars = new ArrayList<>();

	/**
	 * Amount of time the connector will wait, after accepting a connection, for the
	 * request URI line to be presented.
	 */
	private @Nullable Duration connectionTimeout;

	/**
	 * Maximum size of the HTTP response header.
	 */
	private DataSize maxHttpResponseHeaderSize = DataSize.ofKilobytes(8);

	/**
	 * Maximum number of parameters (GET plus POST) that will be automatically parsed by
	 * the container. A value of less than 0 means no limit.
	 */
	private int maxParameterCount = 1000;

	/**
	 * Whether to use APR.
	 */
	private UseApr useApr = UseApr.NEVER;

	/**
	 * Access log configuration.
	 */
	private final Accesslog accesslog = new Accesslog();

	/**
	 * Thread related configuration.
	 */
	private final Threads threads = new Threads();

	/**
	 * Static resource configuration.
	 */
	private final Resource resource = new Resource();

	/**
	 * Modeler MBean Registry configuration.
	 */
	private final Mbeanregistry mbeanregistry = new Mbeanregistry();

	/**
	 * Remote Ip Valve configuration.
	 */
	private final Remoteip remoteip = new Remoteip();

	public Duration getBackgroundProcessorDelay() {
		return this.backgroundProcessorDelay;
	}

	public void setBackgroundProcessorDelay(Duration backgroundProcessorDelay) {
		this.backgroundProcessorDelay = backgroundProcessorDelay;
	}

	public @Nullable File getBasedir() {
		return this.basedir;
	}

	public void setBasedir(@Nullable File basedir) {
		this.basedir = basedir;
	}

	public Boolean getRedirectContextRoot() {
		return this.redirectContextRoot;
	}

	public void setRedirectContextRoot(Boolean redirectContextRoot) {
		this.redirectContextRoot = redirectContextRoot;
	}

	public boolean isUseRelativeRedirects() {
		return this.useRelativeRedirects;
	}

	public void setUseRelativeRedirects(boolean useRelativeRedirects) {
		this.useRelativeRedirects = useRelativeRedirects;
	}

	public Charset getUriEncoding() {
		return this.uriEncoding;
	}

	public void setUriEncoding(Charset uriEncoding) {
		this.uriEncoding = uriEncoding;
	}

	public int getMaxConnections() {
		return this.maxConnections;
	}

	public void setMaxConnections(int maxConnections) {
		this.maxConnections = maxConnections;
	}

	public DataSize getMaxSwallowSize() {
		return this.maxSwallowSize;
	}

	public void setMaxSwallowSize(DataSize maxSwallowSize) {
		this.maxSwallowSize = maxSwallowSize;
	}

	public int getAcceptCount() {
		return this.acceptCount;
	}

	public void setAcceptCount(int acceptCount) {
		this.acceptCount = acceptCount;
	}

	public int getProcessorCache() {
		return this.processorCache;
	}

	public void setProcessorCache(int processorCache) {
		this.processorCache = processorCache;
	}

	public @Nullable Duration getKeepAliveTimeout() {
		return this.keepAliveTimeout;
	}

	public void setKeepAliveTimeout(@Nullable Duration keepAliveTimeout) {
		this.keepAliveTimeout = keepAliveTimeout;
	}

	public int getMaxKeepAliveRequests() {
		return this.maxKeepAliveRequests;
	}

	public void setMaxKeepAliveRequests(int maxKeepAliveRequests) {
		this.maxKeepAliveRequests = maxKeepAliveRequests;
	}

	public List<String> getAdditionalTldSkipPatterns() {
		return this.additionalTldSkipPatterns;
	}

	public void setAdditionalTldSkipPatterns(List<String> additionalTldSkipPatterns) {
		this.additionalTldSkipPatterns = additionalTldSkipPatterns;
	}

	public List<Character> getRelaxedPathChars() {
		return this.relaxedPathChars;
	}

	public void setRelaxedPathChars(List<Character> relaxedPathChars) {
		this.relaxedPathChars = relaxedPathChars;
	}

	public List<Character> getRelaxedQueryChars() {
		return this.relaxedQueryChars;
	}

	public void setRelaxedQueryChars(List<Character> relaxedQueryChars) {
		this.relaxedQueryChars = relaxedQueryChars;
	}

	public @Nullable Duration getConnectionTimeout() {
		return this.connectionTimeout;
	}

	public void setConnectionTimeout(@Nullable Duration connectionTimeout) {
		this.connectionTimeout = connectionTimeout;
	}

	public DataSize getMaxHttpResponseHeaderSize() {
		return this.maxHttpResponseHeaderSize;
	}

	public void setMaxHttpResponseHeaderSize(DataSize maxHttpResponseHeaderSize) {
		this.maxHttpResponseHeaderSize = maxHttpResponseHeaderSize;
	}

	public DataSize getMaxHttpFormPostSize() {
		return this.maxHttpFormPostSize;
	}

	public void setMaxHttpFormPostSize(DataSize maxHttpFormPostSize) {
		this.maxHttpFormPostSize = maxHttpFormPostSize;
	}

	public DataSize getMaxPartHeaderSize() {
		return this.maxPartHeaderSize;
	}

	public void setMaxPartHeaderSize(DataSize maxPartHeaderSize) {
		this.maxPartHeaderSize = maxPartHeaderSize;
	}

	public int getMaxPartCount() {
		return this.maxPartCount;
	}

	public void setMaxPartCount(int maxPartCount) {
		this.maxPartCount = maxPartCount;
	}

	public int getMaxParameterCount() {
		return this.maxParameterCount;
	}

	public void setMaxParameterCount(int maxParameterCount) {
		this.maxParameterCount = maxParameterCount;
	}

	public UseApr getUseApr() {
		return this.useApr;
	}

	public void setUseApr(UseApr useApr) {
		this.useApr = useApr;
	}

	public Accesslog getAccesslog() {
		return this.accesslog;
	}

	public Threads getThreads() {
		return this.threads;
	}

	public Resource getResource() {
		return this.resource;
	}

	public Mbeanregistry getMbeanregistry() {
		return this.mbeanregistry;
	}

	public Remoteip getRemoteip() {
		return this.remoteip;
	}

	/**
	 * Tomcat access log properties.
	 */
	public static class Accesslog {

		/**
		 * Enable access log.
		 */
		private boolean enabled;

		/**
		 * Whether logging of the request will only be enabled if
		 * "ServletRequest.getAttribute(conditionIf)" does not yield null.
		 */
		private @Nullable String conditionIf;

		/**
		 * Whether logging of the request will only be enabled if
		 * "ServletRequest.getAttribute(conditionUnless)" yield null.
		 */
		private @Nullable String conditionUnless;

		/**
		 * Format pattern for access logs.
		 */
		private String pattern = "common";

		/**
		 * Directory in which log files are created. Can be absolute or relative to the
		 * Tomcat base dir.
		 */
		private String directory = "logs";

		/**
		 * Log file name prefix.
		 */
		protected String prefix = "access_log";

		/**
		 * Log file name suffix.
		 */
		private String suffix = ".log";

		/**
		 * Character set used by the log file. Default to the system default character
		 * set.
		 */
		private @Nullable String encoding;

		/**
		 * Locale used to format timestamps in log entries and in log file name suffix.
		 * Default to the default locale of the Java process.
		 */
		private @Nullable String locale;

		/**
		 * Whether to check for log file existence so it can be recreated if an external
		 * process has renamed it.
		 */
		private boolean checkExists;

		/**
		 * Whether to enable access log rotation.
		 */
		private boolean rotate = true;

		/**
		 * Whether to defer inclusion of the date stamp in the file name until rotate
		 * time.
		 */
		private boolean renameOnRotate;

		/**
		 * Number of days to retain the access log files before they are removed.
		 */
		private int maxDays = -1;

		/**
		 * Date format to place in the log file name.
		 */
		private String fileDateFormat = ".yyyy-MM-dd";

		/**
		 * Whether to use IPv6 canonical representation format as defined by RFC 5952.
		 */
		private boolean ipv6Canonical;

		/**
		 * Set request attributes for the IP address, Hostname, protocol, and port used
		 * for the request.
		 */
		private boolean requestAttributesEnabled;

		/**
		 * Whether to buffer output such that it is flushed only periodically.
		 */
		private boolean buffered = true;

		public boolean isEnabled() {
			return this.enabled;
		}

		public void setEnabled(boolean enabled) {
			this.enabled = enabled;
		}

		public @Nullable String getConditionIf() {
			return this.conditionIf;
		}

		public void setConditionIf(@Nullable String conditionIf) {
			this.conditionIf = conditionIf;
		}

		public @Nullable String getConditionUnless() {
			return this.conditionUnless;
		}

		public void setConditionUnless(@Nullable String conditionUnless) {
			this.conditionUnless = conditionUnless;
		}

		public String getPattern() {
			return this.pattern;
		}

		public void setPattern(String pattern) {
			this.pattern = pattern;
		}

		public String getDirectory() {
			return this.directory;
		}

		public void setDirectory(String directory) {
			this.directory = directory;
		}

		public String getPrefix() {
			return this.prefix;
		}

		public void setPrefix(String prefix) {
			this.prefix = prefix;
		}

		public String getSuffix() {
			return this.suffix;
		}

		public void setSuffix(String suffix) {
			this.suffix = suffix;
		}

		public @Nullable String getEncoding() {
			return this.encoding;
		}

		public void setEncoding(@Nullable String encoding) {
			this.encoding = encoding;
		}

		public @Nullable String getLocale() {
			return this.locale;
		}

		public void setLocale(@Nullable String locale) {
			this.locale = locale;
		}

		public boolean isCheckExists() {
			return this.checkExists;
		}

		public void setCheckExists(boolean checkExists) {
			this.checkExists = checkExists;
		}

		public boolean isRotate() {
			return this.rotate;
		}

		public void setRotate(boolean rotate) {
			this.rotate = rotate;
		}

		public boolean isRenameOnRotate() {
			return this.renameOnRotate;
		}

		public void setRenameOnRotate(boolean renameOnRotate) {
			this.renameOnRotate = renameOnRotate;
		}

		public int getMaxDays() {
			return this.maxDays;
		}

		public void setMaxDays(int maxDays) {
			this.maxDays = maxDays;
		}

		public String getFileDateFormat() {
			return this.fileDateFormat;
		}

		public void setFileDateFormat(String fileDateFormat) {
			this.fileDateFormat = fileDateFormat;
		}

		public boolean isIpv6Canonical() {
			return this.ipv6Canonical;
		}

		public void setIpv6Canonical(boolean ipv6Canonical) {
			this.ipv6Canonical = ipv6Canonical;
		}

		public boolean isRequestAttributesEnabled() {
			return this.requestAttributesEnabled;
		}

		public void setRequestAttributesEnabled(boolean requestAttributesEnabled) {
			this.requestAttributesEnabled = requestAttributesEnabled;
		}

		public boolean isBuffered() {
			return this.buffered;
		}

		public void setBuffered(boolean buffered) {
			this.buffered = buffered;
		}

	}

	/**
	 * Tomcat thread properties.
	 */
	public static class Threads {

		/**
		 * Maximum amount of worker threads. Doesn't have an effect if virtual threads are
		 * enabled.
		 */
		private int max = 200;

		/**
		 * Minimum amount of worker threads. Doesn't have an effect if virtual threads are
		 * enabled.
		 */
		private int minSpare = 10;

		/**
		 * Maximum capacity of the thread pool's backing queue. This setting only has an
		 * effect if the value is greater than 0.
		 */
		private int maxQueueCapacity = 2147483647;

		public int getMax() {
			return this.max;
		}

		public void setMax(int max) {
			this.max = max;
		}

		public int getMinSpare() {
			return this.minSpare;
		}

		public void setMinSpare(int minSpare) {
			this.minSpare = minSpare;
		}

		public int getMaxQueueCapacity() {
			return this.maxQueueCapacity;
		}

		public void setMaxQueueCapacity(int maxQueueCapacity) {
			this.maxQueueCapacity = maxQueueCapacity;
		}

	}

	/**
	 * Tomcat static resource properties.
	 */
	public static class Resource {

		/**
		 * Whether static resource caching is permitted for this web application.
		 */
		private boolean allowCaching = true;

		/**
		 * Maximum size of the static resource cache.
		 */
		private DataSize cacheMaxSize = DataSize.ofMegabytes(10);

		/**
		 * Time-to-live of the static resource cache.
		 */
		private Duration cacheTtl = Duration.ofSeconds(5);

		public boolean isAllowCaching() {
			return this.allowCaching;
		}

		public void setAllowCaching(boolean allowCaching) {
			this.allowCaching = allowCaching;
		}

		public DataSize getCacheMaxSize() {
			return this.cacheMaxSize;
		}

		public void setCacheMaxSize(DataSize cacheMaxSize) {
			this.cacheMaxSize = cacheMaxSize;
		}

		public Duration getCacheTtl() {
			return this.cacheTtl;
		}

		public void setCacheTtl(Duration cacheTtl) {
			this.cacheTtl = cacheTtl;
		}

	}

	public static class Mbeanregistry {

		/**
		 * Whether Tomcat's MBean Registry should be enabled.
		 */
		private boolean enabled;

		public boolean isEnabled() {
			return this.enabled;
		}

		public void setEnabled(boolean enabled) {
			this.enabled = enabled;
		}

	}

	public static class Remoteip {

		/**
		 * Internal proxies that are to be trusted. Can be set as a comma separate list of
		 * CIDR or as a regular expression.
		 */
		private String internalProxies = "192.168.0.0/16, 172.16.0.0/12, 169.254.0.0/16, fc00::/7, "
				+ "10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, fe80::/10, ::1/128";

		/**
		 * Header that holds the incoming protocol, usually named "X-Forwarded-Proto".
		 */
		private @Nullable String protocolHeader;

		/**
		 * Value of the protocol header indicating whether the incoming request uses SSL.
		 */
		private String protocolHeaderHttpsValue = "https";

		/**
		 * Name of the HTTP header from which the remote host is extracted.
		 */
		private String hostHeader = "X-Forwarded-Host";

		/**
		 * Name of the HTTP header used to override the original port value.
		 */
		private String portHeader = "X-Forwarded-Port";

		/**
		 * Name of the HTTP header from which the remote IP is extracted. For instance,
		 * 'X-FORWARDED-FOR'.
		 */
		private @Nullable String remoteIpHeader;

		/**
		 * Regular expression defining proxies that are trusted when they appear in the
		 * "remote-ip-header" header.
		 */
		private @Nullable String trustedProxies;

		public String getInternalProxies() {
			return this.internalProxies;
		}

		public void setInternalProxies(String internalProxies) {
			this.internalProxies = internalProxies;
		}

		public @Nullable String getProtocolHeader() {
			return this.protocolHeader;
		}

		public void setProtocolHeader(@Nullable String protocolHeader) {
			this.protocolHeader = protocolHeader;
		}

		public String getProtocolHeaderHttpsValue() {
			return this.protocolHeaderHttpsValue;
		}

		public String getHostHeader() {
			return this.hostHeader;
		}

		public void setHostHeader(String hostHeader) {
			this.hostHeader = hostHeader;
		}

		public void setProtocolHeaderHttpsValue(String protocolHeaderHttpsValue) {
			this.protocolHeaderHttpsValue = protocolHeaderHttpsValue;
		}

		public String getPortHeader() {
			return this.portHeader;
		}

		public void setPortHeader(String portHeader) {
			this.portHeader = portHeader;
		}

		public @Nullable String getRemoteIpHeader() {
			return this.remoteIpHeader;
		}

		public void setRemoteIpHeader(@Nullable String remoteIpHeader) {
			this.remoteIpHeader = remoteIpHeader;
		}

		public @Nullable String getTrustedProxies() {
			return this.trustedProxies;
		}

		public void setTrustedProxies(@Nullable String trustedProxies) {
			this.trustedProxies = trustedProxies;
		}

	}

	/**
	 * When to use APR.
	 */
	public enum UseApr {

		/**
		 * Always use APR and fail if it's not available.
		 */
		ALWAYS,

		/**
		 * Use APR if it is available.
		 */
		WHEN_AVAILABLE,

		/**
		 * Never use APR.
		 */
		NEVER

	}

}
